package org.swift.commons.xml;

import java.io.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.log4j.Logger;
import org.swift.commons.xml.validation.XMLValidationResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


public class XmlUtils {
	private final static Logger log = Logger.getLogger(XmlUtils.class);

	private static final String SCHEMA_FACTORY_IDENTIFIER = XMLConstants.W3C_XML_SCHEMA_NS_URI;

	public static Element readDocument(File file)
			throws ParserConfigurationException, SAXException, IOException {
		return readDocument(new FileInputStream(file), true);
	}

	public static Element readDocument(File file, boolean namespaceAware)
			throws ParserConfigurationException, SAXException, IOException {
		return readDocument(new FileInputStream(file), namespaceAware);
	}

	public static Element readDocument(InputStream inputStream,
			boolean namespaceAware) throws ParserConfigurationException,
			SAXException, IOException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(namespaceAware);
		DocumentBuilder db = dbf.newDocumentBuilder();
		Document doc = db.parse(inputStream);
		return doc.getDocumentElement();
	}

	public static Schema readSchema(File file) throws SAXException {
		return readSchema(new StreamSource(file));
	}

	public static Schema readSchema(Source source) throws SAXException {
		SchemaFactory factory = SchemaFactory
				.newInstance(SCHEMA_FACTORY_IDENTIFIER);
		return factory.newSchema(source);
	}

	public static XMLValidationResult validateXml(File xmlDocument,
			File schemaDocument) throws XMLStreamException,
			FactoryConfigurationError, ParserConfigurationException, SAXException, IOException {
		Element xmlDocumentElement = readDocument(xmlDocument,true);
		Schema schema = readSchema(schemaDocument);
		return validateXml(xmlDocumentElement, schema);
	}

	public static XMLValidationResult validateXml(Element xmlDocument,
			Source schemaDocument) throws IOException, SAXException {
		Schema schema = readSchema(schemaDocument);
		return validateXml(xmlDocument, schema);
	}
	
	public static XMLValidationResult validateXml(Element xmlDocument,
			Schema schemaDocument) throws IOException, SAXException {
		try {
			Validator validator = schemaDocument.newValidator();
			validator.validate(new DOMSource(xmlDocument));
			return new XMLValidationResult(true, "Validation successful.");
		} catch (SAXException saxException) {
			return new XMLValidationResult(false, saxException);
		}
	}

	public static XMLValidationResult validate(Document inputXml, Document schemaAsDocument) 
			throws SAXException, IOException {
		Schema schema = readSchema(new DOMSource(schemaAsDocument.cloneNode(true)));
		return validateXml(inputXml.getDocumentElement(), schema);
	}
	
	public static Node indentXml(Node xml) throws TransformerException, XMLStreamException, ParserConfigurationException, SAXException, IOException {
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "true");
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
		//initialize StreamResult with File object to save to file
		StringWriter writer = new StringWriter();
		StreamResult result = new StreamResult(writer);		
	//	DOMResult result = new DOMResult(); //should work, but for some reason this eliminates the whitespace		
		DOMSource source = new DOMSource(xml);
		transformer.transform(source, result);
		
		return toDocument(writer.toString());
	}
	
	public static String toString(Node doc) {
		DOMSource domSource = new DOMSource(doc);
		StringWriter writer = new StringWriter();
		StreamResult result = new StreamResult(writer);

		TransformerFactory tf = TransformerFactory.newInstance();

		Transformer transformer;
		try {
			transformer = tf.newTransformer();
			transformer.setOutputProperty("omit-xml-declaration", "yes");
			transformer.transform(domSource, result);
		} catch (TransformerException e) {
			throw new RuntimeException(e);
		}
		return writer.toString();
	}

	public static Document toDocument(String xmlSource)
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		DocumentBuilder builder = factory.newDocumentBuilder();		
		return builder.parse(new InputSource(new StringReader(xmlSource)));
	}

	public static Document toDocument(Node node)
			throws ParserConfigurationException, SAXException, 
			IOException, TransformerFactoryConfigurationError, TransformerException {
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");

		StreamResult result = new StreamResult(new StringWriter());
		DOMSource source = new DOMSource(node);
		transformer.transform(source, result);
		String xmlString = result.getWriter().toString();

		return XmlUtils.toDocumentSafe(xmlString);
	}
	
	public static Document createDocument() throws ParserConfigurationException{
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		Document document = factory.newDocumentBuilder().newDocument();
//		factory.
		return document;
	}
	
	public static void eachNode(NodeList nodes, Closure<Node> closure) {
		for (int i = 0; i < nodes.getLength(); i++) {
			closure.execute(nodes.item(i));
		}
	}

	public static void matchElement(NodeList nodes, String name,
			Closure<Element> closure) {
		for (int i = 0; i < nodes.getLength(); i++) {
			if (Node.ELEMENT_NODE == nodes.item(i).getNodeType()
					&& nodes.item(i).getLocalName().equals(name))
				closure.execute((Element) nodes.item(i));
		}
	}

	public static void matchElement(NodeList nodes, String namespace,
			String name, Closure<Element> closure) {
		for (int i = 0; i < nodes.getLength(); i++) {
			if (Node.ELEMENT_NODE == nodes.item(i).getNodeType()
					&& nodes.item(i).getLocalName().equals(name)
					&& namespace.equals(nodes.item(i).getNamespaceURI()))
				closure.execute((Element) nodes.item(i));
		}
	}

	public static void eachElement(NodeList nodes, Closure<Element> closure) {
		for (int i = 0; i < nodes.getLength(); i++) {
			if (Node.ELEMENT_NODE == nodes.item(i).getNodeType())
				closure.execute((Element) nodes.item(i));
		}
	}
	
	public static boolean isElementMatch(NodeList nodes, Match<Element> match) {
		for (int i = 0; i < nodes.getLength(); i++) {
			if (Node.ELEMENT_NODE == nodes.item(i).getNodeType()  
				&& match.isMatch((Element)nodes.item(i)) == true) {
				return true;
			}			
		}
		return false;
	}
	
	public static boolean isMatch(NodeList nodes, Match<Node> match) {
		for (int i = 0; i < nodes.getLength(); i++) {
			if (match.isMatch(nodes.item(i)) == true) {
				return true;
			}			
		}
		return false;
	}
	
	public static interface Match<T> {
		public boolean isMatch(T node);
	}

	public static interface Closure<T> {
		public void execute(T item);
	}
	
	
	
	
//	@SuppressWarnings("unchecked")
//	public static Node toNode(Object valueObject){		
//		Class clazz = valueObject.getClass();
//		if (Node.class.isAssignableFrom(clazz)){
//			return (Node)valueObject;
//		} else if (Collection.class.isAssignableFrom(clazz)){
//			Node results = toNode("<results />");
//			for (Object value : (Collection) valueObject){
//				results.appendChild(toNode(value));
//			}
//			return results;
//		} else if (String.class.isAssignableFrom(clazz)){
//			return toNode((String)valueObject);
//		}
//		Document doc = toDocument(valueObject);
//		if (doc!=null)return doc.getDocumentElement();
//		return null;
//	}
	
	public static Node toNode(String valueString){
		Document doc = toDocumentSafe(valueString);
		if (doc!=null)return doc.getFirstChild();
		return null;
	}
	
//	@SuppressWarnings("unchecked")
//	public static Document toDocument(Object valueObject){		
//		Class clazz = valueObject.getClass();
//		if (Node.class.isAssignableFrom(clazz)){
//			return toDocument((Node)valueObject);
//		} else if (Collection.class.isAssignableFrom(clazz)){
//			return toDocument((Collection)valueObject);
//		} 
//		
//		return toDocumentSafe (valueObject.toString());
//	}
	
	public static Document toDocumentSafe (String valueString){
		if (!XmlUtils.isXml(valueString)){
			valueString = toXML(valueString);
		}
		try {
			return toDocument(valueString);
		} catch (Throwable e) {
			log.warn("could not parse string to xml:" +  e);
			log.debug(e);
		}
		return null;
	}
	
	public static Document toDocument(Document valueDoc){
		return valueDoc;
	}
	
	public static Document toDocument(String rootElement, Collection<?> valueCollection) throws ParserConfigurationException, SAXException, IOException{
		String xml = toXML(rootElement, valueCollection);
		return toDocument(xml);		
	}
	
	public static String toXML(String value){
		return "<results>" + value + "</results>";
	}
	
	@SuppressWarnings("unchecked")
	public static String toXML(String rootElement, Object value){
		Object val = value;
		Class clazz = val.getClass();		
		if (Node.class.isAssignableFrom(clazz)){
			return toXML((Node)val);
		} else if (Collection.class.isAssignableFrom(clazz)){
			return toXML(rootElement,(Collection)val);
		}
		return "<" + rootElement + ">" + val + "</" + rootElement + ">";
	}
	
	public static String toXML(String rootElement, Collection<?> valueCollection){
		StringBuffer strbuff = new StringBuffer();
		strbuff.append("<" + rootElement + "s>");
		for (Object valueObject:valueCollection){
			strbuff.append(toXML(rootElement,valueObject));
		}
		strbuff.append("</" + rootElement + "s>");
		return strbuff.toString();
	}
		
	private static String toXML(Node valueNode){
		return XmlUtils.toString(valueNode);
	}
	
	public static void printNodeAndTypes(StringBuffer buffer, Node node, int spaces) {
		StringBuffer szSpaces = new StringBuffer();
		for (int i = 0; i < spaces * 2; i++)
			szSpaces.append(" ");
		buffer.append(szSpaces + node.getNodeName() + " | "
			+ node.getNodeType() + " | " + node.getNodeValue() + "\n");
		NodeList childNodes = node.getChildNodes();
		for (int i = 0; i < childNodes.getLength(); i++)
			printNodeAndTypes(buffer, childNodes.item(i), spaces + 1);
	}

	public static String printNodeAndTypes(Node node) {
		StringBuffer results = new StringBuffer();
		printNodeAndTypes(results, node, 0);
		return results.toString();
	}
	
	public static boolean isXml(String string) {
		return ((string.startsWith("<") || string.trim().startsWith("<")) 
				&& (string.endsWith(">") || string.trim().endsWith(">")));
	}

    public static Map<String,Node> getNamespaceAttributes(Document document) {
        if ( document == null ) {
            return new HashMap<String,Node>();
        }
        return getNamespaceAttributes(document.getFirstChild());
    }

    public static Map<String,Node> getNamespaceAttributes(Node node) {
        Map<String,Node> map = new HashMap<String,Node>();

        if ( node != null && node.getAttributes() != null ) {
            final int count = node.getAttributes().getLength();
            for ( int i = 0; i < count; i++ ) {
                Node attr = node.getAttributes().item(i);

                if ( attr.getNodeName().startsWith("xmlns") ) {
                    map.put(attr.getNodeName(), attr);
                }
            }
        }

        return map;
    }

    public static Document cloneDocument(Document original) throws IOException {
        Document newDoc = null;
        ByteArrayOutputStream bos = null;
        ObjectOutputStream oos = null;
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;

        try {
            bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
            oos.writeObject(original);
            oos.flush();
            closeStream(oos);
            closeStream(bos);

            bais = new ByteArrayInputStream(bos.toByteArray());
            ois = new ObjectInputStream(bais);
            newDoc = (Document) ois.readObject();
        }
        catch (ClassNotFoundException e) {
            throw new IOException("Failed to clone document: "+e.getMessage(), e);
        }
        finally {
            closeStream(bos);
            closeStream(oos);
            closeStream(bais);
            closeStream(ois);
        }

        return newDoc;
    }

    private static void closeStream(InputStream stream) {
        try {
            if ( stream != null ) {
                stream.close();
            }
        }
        catch (Exception e) {
            log.debug(e.getMessage(), e);
        }
    }

    private static void closeStream(OutputStream stream) {
        try {
            if ( stream != null ) {
                stream.close();
            }
        }
        catch (Exception e) {
            log.debug(e.getMessage(), e);
        }
    }
}
